Micron Document
πŸŽ–οΈGitΠ―Ρ€Π°πŸŽ–οΈ


Displaying Raw β€’ Download

feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt fix/qr-error-correction (7b45f84f) Text, 33.85 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.feature.connections

Tff7b72import T7ee787app.cash.turbine.test
Tff7b72import T7ee787dev.mokkery.answering.returns
Tff7b72import T7ee787dev.mokkery.every
Tff7b72import T7ee787dev.mokkery.matcher.any
Tff7b72import T7ee787dev.mokkery.verifySuspend
Tff7b72import T7ee787kotlinx.coroutines.Dispatchers
Tff7b72import T7ee787kotlinx.coroutines.flow.MutableStateFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.filterNotNull
Tff7b72import T7ee787kotlinx.coroutines.flow.flow
Tff7b72import T7ee787kotlinx.coroutines.test.resetMain
Tff7b72import T7ee787kotlinx.coroutines.test.runTest
Tff7b72import T7ee787kotlinx.coroutines.test.setMain
Tff7b72import T7ee787org.meshtastic.core.ble.BleDevice
Tff7b72import T7ee787org.meshtastic.core.ble.BleScanStartException
Tff7b72import T7ee787org.meshtastic.core.ble.BleScanStartFailureReason
Tff7b72import T7ee787org.meshtastic.core.common.util.safeCatchingAll
Tff7b72import T7ee787org.meshtastic.core.model.ConnectionState
Tff7b72import T7ee787org.meshtastic.core.model.DeviceType
Tff7b72import T7ee787org.meshtastic.core.network.repository.DiscoveredService
Tff7b72import T7ee787org.meshtastic.core.resources.Res
Tff7b72import T7ee787org.meshtastic.core.resources.bluetooth_disabled
Tff7b72import T7ee787org.meshtastic.core.resources.bluetooth_scan_location_services_disabled
Tff7b72import T7ee787org.meshtastic.core.resources.bluetooth_scan_missing_permission
Tff7b72import T7ee787org.meshtastic.core.resources.bluetooth_scan_start_failed
Tff7b72import T7ee787org.meshtastic.core.resources.bluetooth_scan_too_frequent
Tff7b72import T7ee787org.meshtastic.core.resources.getPluralStringSuspend
Tff7b72import T7ee787org.meshtastic.core.resources.getStringSuspend
Tff7b72import T7ee787org.meshtastic.core.testing.FakeBleDevice
Tff7b72import T7ee787org.meshtastic.feature.connections.model.DeviceListEntry
Tff7b72import T7ee787org.meshtastic.feature.connections.model.DiscoveredDevices
Tff7b72import T7ee787kotlin.test.AfterTest
Tff7b72import T7ee787kotlin.test.BeforeTest
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertNotNull
Tff7b72import T7ee787kotlin.time.Duration.Companion.milliseconds
Tff7b72import T7ee787kotlin.time.Duration.Companion.seconds

Tf0883e@OptInTb4b4b4(Te6edf3kotlinxTb4b4b4.Te6edf3coroutinesTb4b4b4.Te6edf3ExperimentalCoroutinesApiTff7b72::Te6edf3classTb4b4b4)
Tff7b72class T56d364ScannerViewModelTest Tb4b4b4{

Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3harnessTb4b4b4: Te6edf3ScannerViewModelHarness
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3viewModelTb4b4b4: Te6edf3ScannerViewModel

T8b949e// Convenience aliases so the existing test bodies read unchanged.
Tff7b72private Tff7b72val Te6edf3serviceRepository
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3serviceRepository

Tff7b72private Tff7b72val Te6edf3radioController
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3radioController

Tff7b72private Tff7b72val Te6edf3bleScanner
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3bleScanner

Tff7b72private Tff7b72val Te6edf3baseDevicesFlow
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3baseDevicesFlow

Tff7b72private Tff7b72val Te6edf3resolvedServicesFlow
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3resolvedServicesFlow

Tf0883e@BeforeTest
Tff7b72fun Td2a8ffsetUpTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3harness Tff7b72= Te6edf3ScannerViewModelHarnessTb4b4b4(Tb4b4b4)
Te6edf3DispatchersTb4b4b4.Te6edf3setMainTb4b4b4(Te6edf3harnessTb4b4b4.Te6edf3testDispatcherTb4b4b4)

Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionProgressTb4b4b4(Ta5d6ff"Ta5d6ff"Tb4b4b4)
Te6edf3baseDevicesFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3DiscoveredDevicesTb4b4b4(Tb4b4b4)
Te6edf3resolvedServicesFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3emptyListTb4b4b4(Tb4b4b4)

Te6edf3viewModel Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3buildBaseTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tf0883e@AfterTest
Tff7b72fun Td2a8fftearDownTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Order matters: the ViewModel's coroutines must be gone before Main is unset.
Te6edf3harnessTb4b4b4.Te6edf3clearViewModelTb4b4b4(Te6edf3viewModelTb4b4b4)
Te6edf3DispatchersTb4b4b4.Te6edf3resetMainTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8fftestInitializationTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertNotNullTb4b4b4(Te6edf3viewModelTb4b4b4)
Tb4b4b4}

T8b949e/**
* Demo Mode's gate opens mid-session in a release build, when the user performs the hidden-features gesture in
* Settings. Sampling it once in `init` (as this used to) meant the Connections list never noticed.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`showMockTransport follows the transport gate after construction`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3viewModelTb4b4b4.Te6edf3showMockTransportTb4b4b4.Te6edf3test Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4)

Te6edf3harnessTb4b4b4.Te6edf3mockTransportEnabledTb4b4b4.Te6edf3value Tff7b72= Tff7b72true
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4)

Te6edf3harnessTb4b4b4.Te6edf3mockTransportEnabledTb4b4b4.Te6edf3value Tff7b72= Tff7b72false
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3cancelAndIgnoreRemainingEventsTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

T8b949e/**
* The gate has to reach the device list, not merely be observable on the ViewModel.
*
* Asserting on `showMockTransport` alone would pass even if the ViewModel stopped feeding the gate into the
* device-list query, so this asserts on the requests the use case actually received: one per gate value, in order.
* A gate sampled once at construction β€” which is what this branch fixes β€” records only its initial value here.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`a mid-session unlock re-queries the device list`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3viewModelTb4b4b4.Te6edf3usbDevicesForUiTb4b4b4.Te6edf3test Tb4b4b4{
Te6edf3awaitItemTb4b4b4(Tb4b4b4)
Te6edf3testSchedulerTb4b4b4.Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3listOfTb4b4b4(Tff7b72false Te6edf3to Tff7b72falseTb4b4b4)Tb4b4b4,
Te6edf3harnessTb4b4b4.Te6edf3discoveryRequestsTb4b4b4,
Ta5d6ff"Ta5d6ffa locked gate must still have queried the device list onceTa5d6ff"Tb4b4b4,
Tb4b4b4)

T8b949e// Each transition is checkpointed before the next one is provoked. Writing `true` and `false` back to
T8b949e// back would let the StateFlow conflate them, and the `true` request β€” the one this whole feature exists
T8b949e// to produce β€” could then never be observed, leaving the test green but vacuous. Waiting here fixes the
T8b949e// ordering through the test's own control flow rather than through dispatcher timing, which is not a
T8b949e// contract worth asserting on.
Te6edf3harnessTb4b4b4.Te6edf3mockTransportEnabledTb4b4b4.Te6edf3value Tff7b72= Tff7b72true
Te6edf3testSchedulerTb4b4b4.Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3listOfTb4b4b4(Tff7b72false Te6edf3to Tff7b72falseTb4b4b4, Tff7b72true Te6edf3to Tff7b72falseTb4b4b4)Tb4b4b4,
Te6edf3harnessTb4b4b4.Te6edf3discoveryRequestsTb4b4b4,
Ta5d6ff"Ta5d6ffunlocking Demo Mode mid-session must re-query the device listTa5d6ff"Tb4b4b4,
Tb4b4b4)

Te6edf3harnessTb4b4b4.Te6edf3mockTransportEnabledTb4b4b4.Te6edf3value Tff7b72= Tff7b72false
Te6edf3testSchedulerTb4b4b4.Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3listOfTb4b4b4(Tff7b72false Te6edf3to Tff7b72falseTb4b4b4, Tff7b72true Te6edf3to Tff7b72falseTb4b4b4, Tff7b72false Te6edf3to Tff7b72falseTb4b4b4)Tb4b4b4,
Te6edf3harnessTb4b4b4.Te6edf3discoveryRequestsTb4b4b4,
Ta5d6ff"Ta5d6ffre-locking must re-query it againTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3cancelAndIgnoreRemainingEventsTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`connectionProgressText reflects connectionProgress`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3viewModelTb4b4b4.Te6edf3connectionProgressTextTb4b4b4.Te6edf3test Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ff"Tb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionProgressTb4b4b4(Ta5d6ff"Ta5d6ffConnecting...Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffConnecting...Ta5d6ff"Tb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3cancelAndIgnoreRemainingEventsTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`startBleScan updates isBleScanning`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3test Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3stopBleScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3cancelAndIgnoreRemainingEventsTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`scan startup failure clears scanning state disables auto-scan and surfaces error`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3warmScanFailureStringsTb4b4b4(Tb4b4b4)
Te6edf3harnessTb4b4b4.Te6edf3uiPrefsTb4b4b4.Te6edf3setBleAutoScanTb4b4b4(Tff7b72trueTb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3bleScannerTb4b4b4.Te6edf3scanTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns Te6edf3failingScanFlowTb4b4b4(Tb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3bleAutoScanTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Ta5d6ff"Ta5d6ffBluetooth scan couldn't start. Try again, or toggle Bluetooth if the problem continues.Ta5d6ff"Tb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3errorMessageTb4b4b4.Te6edf3valueTb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`scan startup failure cooldown prevents immediate retry and allows later manual retry`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72var Te6edf3scanAttempts Tff7b72= T79c0ff0
Te6edf3every Tb4b4b4{ Te6edf3bleScannerTb4b4b4.Te6edf3scanTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns
Te6edf3flow Tb4b4b4{
Te6edf3scanAttempts Tff7b72+Tff7b72= T79c0ff1
Tff7b72throw Te6edf3BleScanStartExceptionTb4b4b4(
Te6edf3reason Tff7b72= Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3ApplicationRegistrationFailedTb4b4b4,
Te6edf3cause Tff7b72= Te6edf3IllegalStateExceptionTb4b4b4(Ta5d6ff"Ta5d6ffFailed to start scan as app cannot be registeredTa5d6ff"Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3scanAttemptsTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3scanAttemptsTb4b4b4)

Te6edf3harnessTb4b4b4.Te6edf3testDispatcherTb4b4b4.Te6edf3schedulerTb4b4b4.Te6edf3advanceTimeByTb4b4b4(Te6edf3BLE_SCAN_START_FAILURE_RETRY_COOLDOWNTb4b4b4.Te6edf3inWholeMilliseconds Tff7b72+ T79c0ff1Tb4b4b4)
Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(T79c0ff2Tb4b4b4, Te6edf3scanAttemptsTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`bluetooth-disabled failure allows an immediate retry once the user re-enables it`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3warmScanFailureStringsTb4b4b4(Tb4b4b4)
T8b949e// No cooldown for preconditions the user clears with a system toggle β€” a dead scan button right after they
T8b949e// switched Bluetooth back on reads as a broken app.
Tff7b72var Te6edf3scanAttempts Tff7b72= T79c0ff0
Te6edf3every Tb4b4b4{ Te6edf3bleScannerTb4b4b4.Te6edf3scanTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns
Te6edf3flow Tb4b4b4{
Te6edf3scanAttempts Tff7b72+Tff7b72= T79c0ff1
Tff7b72throw Te6edf3BleScanStartExceptionTb4b4b4(
Te6edf3reason Tff7b72= Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3BluetoothDisabledTb4b4b4,
Te6edf3cause Tff7b72= Te6edf3IllegalStateExceptionTb4b4b4(Ta5d6ff"Ta5d6ffBluetooth disabledTa5d6ff"Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3scanAttemptsTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffBluetooth is off. Turn it on to scan for nearby devices.Ta5d6ff"Tb4b4b4, Te6edf3serviceRepositoryTb4b4b4.Te6edf3errorMessageTb4b4b4.Te6edf3valueTb4b4b4)

T8b949e// Immediately retryable β€” no waiting on the scheduler.
Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff2Tb4b4b4, Te6edf3scanAttemptsTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`location-services-disabled failure allows an immediate retry`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3warmScanFailureStringsTb4b4b4(Tb4b4b4)
Tff7b72var Te6edf3scanAttempts Tff7b72= T79c0ff0
Te6edf3every Tb4b4b4{ Te6edf3bleScannerTb4b4b4.Te6edf3scanTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns
Te6edf3flow Tb4b4b4{
Te6edf3scanAttempts Tff7b72+Tff7b72= T79c0ff1
Tff7b72throw Te6edf3BleScanStartExceptionTb4b4b4(
Te6edf3reason Tff7b72= Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3LocationServicesDisabledTb4b4b4,
Te6edf3cause Tff7b72= Te6edf3IllegalStateExceptionTb4b4b4(Ta5d6ff"Ta5d6ffLocation services are required for scanning but are disabledTa5d6ff"Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3scanAttemptsTb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Ta5d6ff"Ta5d6ffLocation services are off. Turn them on to scan for nearby devices.Ta5d6ff"Tb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3errorMessageTb4b4b4.Te6edf3valueTb4b4b4,
Tb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff2Tb4b4b4, Te6edf3scanAttemptsTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`scan quota failure honors retry-after cooldown`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3warmScanFailureStringsTb4b4b4(Tb4b4b4)
Tff7b72var Te6edf3scanAttempts Tff7b72= T79c0ff0
Te6edf3every Tb4b4b4{ Te6edf3bleScannerTb4b4b4.Te6edf3scanTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns
Te6edf3flow Tb4b4b4{
Te6edf3scanAttempts Tff7b72+Tff7b72= T79c0ff1
Tff7b72throw Te6edf3BleScanStartExceptionTb4b4b4(
Te6edf3reason Tff7b72= Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3ScanningTooFrequentlyTb4b4b4,
Te6edf3cause Tff7b72= Te6edf3IllegalStateExceptionTb4b4b4(Ta5d6ff"Ta5d6ffAndroid BLE scan-start quota exhaustedTa5d6ff"Tb4b4b4)Tb4b4b4,
Te6edf3retryAfter Tff7b72= T79c0ff3T79c0ff0.Te6edf3seconds Tff7b72+ T79c0ff1.Te6edf3millisecondsTb4b4b4,
Tb4b4b4)
Tb4b4b4}

Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3scanAttemptsTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffBluetooth scan limit reached. Try again in 31 seconds.Ta5d6ff"Tb4b4b4, Te6edf3serviceRepositoryTb4b4b4.Te6edf3errorMessageTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3harnessTb4b4b4.Te6edf3testDispatcherTb4b4b4.Te6edf3schedulerTb4b4b4.Te6edf3advanceTimeByTb4b4b4(Te6edf3BLE_SCAN_START_FAILURE_RETRY_COOLDOWNTb4b4b4.Te6edf3inWholeMilliseconds Tff7b72+ T79c0ff1Tb4b4b4)
Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3scanAttemptsTb4b4b4)

Te6edf3harnessTb4b4b4.Te6edf3testDispatcherTb4b4b4.Te6edf3schedulerTb4b4b4.Te6edf3advanceTimeByTb4b4b4(T79c0ff1T79c0ff5.Te6edf3secondsTb4b4b4.Te6edf3inWholeMilliseconds Tff7b72+ T79c0ff1Tb4b4b4)
Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff2Tb4b4b4, Te6edf3scanAttemptsTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`stopBleScan is idempotent after failed startup`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3every Tb4b4b4{ Te6edf3bleScannerTb4b4b4.Te6edf3scanTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns Te6edf3failingScanFlowTb4b4b4(Tb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)
Te6edf3viewModelTb4b4b4.Te6edf3stopBleScanTb4b4b4(Tb4b4b4)
Te6edf3viewModelTb4b4b4.Te6edf3stopBleScanTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`changeDeviceAddress calls radioController`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3viewModelTb4b4b4.Te6edf3changeDeviceAddressTb4b4b4(Ta5d6ff"Ta5d6fftest_addressTa5d6ff"Tb4b4b4)
Te6edf3testSchedulerTb4b4b4.Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6fftest_addressTa5d6ff"Tb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`dismissRecovery clears the pending recovery record`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
T8b949e// User dismisses a recovery that can't succeed (e.g. an unflashable bootloader) β€” the record
T8b949e// must be cleared so the banner stops nagging.
Te6edf3viewModelTb4b4b4.Te6edf3dismissRecoveryTb4b4b4(Tb4b4b4)
Te6edf3testSchedulerTb4b4b4.Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3verifySuspend Tb4b4b4{ Te6edf3harnessTb4b4b4.Te6edf3firmwareRecoveryDataSourceTb4b4b4.Te6edf3clearTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`usbDevicesForUi emits updates`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3viewModelTb4b4b4.Te6edf3usbDevicesForUiTb4b4b4.Te6edf3test Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4)

Tff7b72val Te6edf3device Tff7b72=
Te6edf3DeviceListEntryTb4b4b4.Te6edf3UsbTb4b4b4(
Te6edf3usbData Tff7b72= Tff7b72object Tb4b4b4: T56d364orgTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3featureTb4b4b4.Te6edf3connectionsTb4b4b4.Te6edf3modelTb4b4b4.Te6edf3UsbDeviceData Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffUSB DeviceTa5d6ff"Tb4b4b4,
Te6edf3fullAddress Tff7b72= Ta5d6ff"Ta5d6ffusb_addressTa5d6ff"Tb4b4b4,
Te6edf3bonded Tff7b72= Tff7b72trueTb4b4b4,
Tb4b4b4)
Te6edf3baseDevicesFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3DiscoveredDevicesTb4b4b4(Te6edf3usbDevices Tff7b72= Te6edf3listOfTb4b4b4(Te6edf3deviceTb4b4b4)Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3deviceTb4b4b4)Tb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3cancelAndIgnoreRemainingEventsTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`isNetworkScanning defaults to false`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`active transport defaults to BLE when no selected device or preference exists`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Te6edf3DeviceTypeTb4b4b4.Te6edf3BLETb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3activeTransportTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`active transport defaults to selected device type when no explicit preference exists`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3harnessTb4b4b4.Te6edf3currentDeviceAddressFlowTb4b4b4.Te6edf3value Tff7b72= Ta5d6ff"Ta5d6fft192.168.1.50Ta5d6ff"

Te6edf3assertEqualsTb4b4b4(Te6edf3DeviceTypeTb4b4b4.Te6edf3TCPTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3activeTransportTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`explicit active transport preference wins over selected device type`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3harnessTb4b4b4.Te6edf3currentDeviceAddressFlowTb4b4b4.Te6edf3value Tff7b72= Ta5d6ff"Ta5d6ffs/dev/bus/usb/001/002Ta5d6ff"

Te6edf3viewModelTb4b4b4.Te6edf3selectTransportTb4b4b4(Te6edf3DeviceTypeTb4b4b4.Te6edf3BLETb4b4b4)

Te6edf3assertEqualsTb4b4b4(Te6edf3DeviceTypeTb4b4b4.Te6edf3BLETb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3activeTransportTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`startNetworkScan updates isNetworkScanning`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3test Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3viewModelTb4b4b4.Te6edf3startNetworkScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3viewModelTb4b4b4.Te6edf3stopNetworkScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3cancelAndIgnoreRemainingEventsTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`discoveredTcpDevicesForUi is empty when not scanning`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3resolvedServicesFlowTb4b4b4.Te6edf3value Tff7b72=
Te6edf3listOfTb4b4b4(Te6edf3DiscoveredServiceTb4b4b4(Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffNSD DeviceTa5d6ff"Tb4b4b4, Te6edf3hostAddress Tff7b72= Ta5d6ff"Ta5d6ff192.168.1.50Ta5d6ff"Tb4b4b4, Te6edf3port Tff7b72= T79c0ff4T79c0ff4T79c0ff0T79c0ff3Tb4b4b4)Tb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3discoveredTcpDevicesForUiTb4b4b4.Te6edf3test Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3cancelAndIgnoreRemainingEventsTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`discoveredTcpDevicesForUi populates when scanning is active`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3resolvedServicesFlowTb4b4b4.Te6edf3value Tff7b72=
Te6edf3listOfTb4b4b4(Te6edf3DiscoveredServiceTb4b4b4(Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffNSD DeviceTa5d6ff"Tb4b4b4, Te6edf3hostAddress Tff7b72= Ta5d6ff"Ta5d6ff192.168.1.50Ta5d6ff"Tb4b4b4, Te6edf3port Tff7b72= T79c0ff4T79c0ff4T79c0ff0T79c0ff3Tb4b4b4)Tb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3discoveredTcpDevicesForUiTb4b4b4.Te6edf3test Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3viewModelTb4b4b4.Te6edf3startNetworkScanTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3awaitItemTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3sizeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6fft192.168.1.50Ta5d6ff"Tb4b4b4, Te6edf3resultTff7b72[T79c0ff0Tff7b72]Tb4b4b4.Te6edf3fullAddressTb4b4b4)
Te6edf3viewModelTb4b4b4.Te6edf3stopNetworkScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3cancelAndIgnoreRemainingEventsTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

T8b949e/**
* The replay demo entry only makes sense in a build carrying the capture asset; without it the replay transport
* degrades to the plain mock. These two pin the ViewModel as the thing that forwards that capability, so the gate
* cannot regress into "always offer replay whenever Demo Mode is on".
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`replay is requested only when the transport reports the capture asset`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Te6edf3listOfTb4b4b4(Tff7b72true Te6edf3to Tff7b72trueTb4b4b4)Tb4b4b4, Te6edf3requestedVisibilityTb4b4b4(Te6edf3mockTransport Tff7b72= Tff7b72trueTb4b4b4, Te6edf3replayAvailable Tff7b72= Tff7b72trueTb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`replay is not requested when the capture asset is absent`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Te6edf3listOfTb4b4b4(Tff7b72true Te6edf3to Tff7b72falseTb4b4b4)Tb4b4b4, Te6edf3requestedVisibilityTb4b4b4(Te6edf3mockTransport Tff7b72= Tff7b72trueTb4b4b4, Te6edf3replayAvailable Tff7b72= Tff7b72falseTb4b4b4)Tb4b4b4)
Tb4b4b4}

T8b949e/**
* Builds a ViewModel against the given transport capabilities and returns the distinct `(showMock, showReplay)`
* pairs it asked the use case for. A fresh ViewModel is required because the replay flag is latched in `init` β€” the
* Demo Mode gate itself is observed, so it is set on the backing flow rather than stubbed.
*/
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffrequestedVisibilityTb4b4b4(
Te6edf3mockTransportTb4b4b4: Tffa657BooleanTb4b4b4,
Te6edf3replayAvailableTb4b4b4: Tffa657BooleanTb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3PairTff7b72<Tffa657BooleanTb4b4b4, Tffa657BooleanTff7b72>Tff7b72> Tb4b4b4{
Te6edf3harnessTb4b4b4.Te6edf3mockTransportEnabledTb4b4b4.Te6edf3value Tff7b72= Te6edf3mockTransport
Te6edf3every Tb4b4b4{ Te6edf3harnessTb4b4b4.Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3isReplayTransportAvailable Tb4b4b4} Te6edf3returns Te6edf3replayAvailable
Te6edf3harnessTb4b4b4.Te6edf3discoveryRequestsTb4b4b4.Te6edf3clearTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3subject Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3buildBaseTb4b4b4(Tb4b4b4)
Tff7b72try Tb4b4b4{
Te6edf3subjectTb4b4b4.Te6edf3usbDevicesForUiTb4b4b4.Te6edf3test Tb4b4b4{
Te6edf3awaitItemTb4b4b4(Tb4b4b4)
Te6edf3cancelAndIgnoreRemainingEventsTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tff7b72return Te6edf3harnessTb4b4b4.Te6edf3discoveryRequestsTb4b4b4.Te6edf3distinctTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72finally Tb4b4b4{
Te6edf3harnessTb4b4b4.Te6edf3clearViewModelTb4b4b4(Te6edf3subjectTb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`bleDevicesForUi shows bonded devices only once they are visible via scan`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3device1 Tff7b72= Te6edf3FakeBleDeviceTb4b4b4(Te6edf3address Tff7b72= Ta5d6ff"Ta5d6ff01:02:03:04:05:06Ta5d6ff"Tb4b4b4, Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffNode BTa5d6ff"Tb4b4b4, Te6edf3rssi Tff7b72= Tff7b72-T79c0ff5T79c0ff0Tb4b4b4)
Tff7b72val Te6edf3device2 Tff7b72= Te6edf3FakeBleDeviceTb4b4b4(Te6edf3address Tff7b72= Ta5d6ff"Ta5d6ff07:08:09:0A:0B:0CTa5d6ff"Tb4b4b4, Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffNode ATa5d6ff"Tb4b4b4, Te6edf3rssi Tff7b72= Tff7b72-T79c0ff3T79c0ff0Tb4b4b4)
Tff7b72val Te6edf3bondedBle Tff7b72= Te6edf3FakeBleDeviceTb4b4b4(Te6edf3address Tff7b72= Ta5d6ff"Ta5d6ff0D:0E:0F:10:11:12Ta5d6ff"Tb4b4b4, Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffBonded CTa5d6ff"Tb4b4b4, Te6edf3rssi Tff7b72= Tff7b72nullTb4b4b4)
Tff7b72val Te6edf3bondedDevice Tff7b72= Te6edf3DeviceListEntryTb4b4b4.Te6edf3BleTb4b4b4(Te6edf3device Tff7b72= Te6edf3bondedBleTb4b4b4, Te6edf3bonded Tff7b72= Tff7b72trueTb4b4b4)

Tff7b72val Te6edf3scanFlow Tff7b72= Te6edf3MutableStateFlowTff7b72<Te6edf3BleDevice?Tff7b72>Tb4b4b4(Tff7b72nullTb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3bleScannerTb4b4b4.Te6edf3scanTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns Te6edf3scanFlowTb4b4b4.Te6edf3filterNotNullTb4b4b4(Tb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3bleDevicesForUiTb4b4b4.Te6edf3test Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4)

T8b949e// A system-bonded device that isn't advertising stays hidden β€” the list only shows what's nearby.
Te6edf3baseDevicesFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3DiscoveredDevicesTb4b4b4(Te6edf3bleDevices Tff7b72= Te6edf3listOfTb4b4b4(Te6edf3bondedDeviceTb4b4b4)Tb4b4b4)
Te6edf3expectNoEventsTb4b4b4(Tb4b4b4)

T8b949e// 1. Scan finds Device 1 (Node B) β€” unbonded, appears and routes through bonding when tapped.
Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)
Te6edf3scanFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3device1
Tff7b72val Te6edf3afterDevice1 Tff7b72= Te6edf3awaitItemTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3afterDevice1Tb4b4b4.Te6edf3sizeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3device1Tb4b4b4.Te6edf3addressTb4b4b4, Tb4b4b4(Te6edf3afterDevice1Tff7b72[T79c0ff0Tff7b72] Tff7b72as Te6edf3DeviceListEntryTb4b4b4.Te6edf3BleTb4b4b4)Tb4b4b4.Te6edf3addressTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3afterDevice1Tff7b72[T79c0ff0Tff7b72]Tb4b4b4.Te6edf3bondedTb4b4b4)

T8b949e// 2. Scan finds Device 2 (Node A, -30dBm) - stronger signal but kept AFTER Device 1 per discovery order.
Te6edf3scanFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3device2
Tff7b72val Te6edf3afterDevice2 Tff7b72= Te6edf3awaitItemTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff2Tb4b4b4, Te6edf3afterDevice2Tb4b4b4.Te6edf3sizeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3device1Tb4b4b4.Te6edf3addressTb4b4b4, Tb4b4b4(Te6edf3afterDevice2Tff7b72[T79c0ff0Tff7b72] Tff7b72as Te6edf3DeviceListEntryTb4b4b4.Te6edf3BleTb4b4b4)Tb4b4b4.Te6edf3addressTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3device2Tb4b4b4.Te6edf3addressTb4b4b4, Tb4b4b4(Te6edf3afterDevice2Tff7b72[T79c0ff1Tff7b72] Tff7b72as Te6edf3DeviceListEntryTb4b4b4.Te6edf3BleTb4b4b4)Tb4b4b4.Te6edf3addressTb4b4b4)

T8b949e// 3. The bonded device starts advertising β€” now it appears, flagged bonded and sorted first by name.
Te6edf3scanFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3bondedBle
Tff7b72val Te6edf3afterBonded Tff7b72= Te6edf3awaitItemTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff3Tb4b4b4, Te6edf3afterBondedTb4b4b4.Te6edf3sizeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3bondedDeviceTb4b4b4.Te6edf3addressTb4b4b4, Tb4b4b4(Te6edf3afterBondedTff7b72[T79c0ff0Tff7b72] Tff7b72as Te6edf3DeviceListEntryTb4b4b4.Te6edf3BleTb4b4b4)Tb4b4b4.Te6edf3addressTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3afterBondedTff7b72[T79c0ff0Tff7b72]Tb4b4b4.Te6edf3bondedTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3device1Tb4b4b4.Te6edf3addressTb4b4b4, Tb4b4b4(Te6edf3afterBondedTff7b72[T79c0ff1Tff7b72] Tff7b72as Te6edf3DeviceListEntryTb4b4b4.Te6edf3BleTb4b4b4)Tb4b4b4.Te6edf3addressTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3device2Tb4b4b4.Te6edf3addressTb4b4b4, Tb4b4b4(Te6edf3afterBondedTff7b72[T79c0ff2Tff7b72] Tff7b72as Te6edf3DeviceListEntryTb4b4b4.Te6edf3BleTb4b4b4)Tb4b4b4.Te6edf3addressTb4b4b4)

T8b949e// 4. Device 1 RSSI updates to -20dBm (strongest) - should NOT re-sort.
Te6edf3scanFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3FakeBleDeviceTb4b4b4(Te6edf3address Tff7b72= Te6edf3device1Tb4b4b4.Te6edf3addressTb4b4b4, Te6edf3name Tff7b72= Te6edf3device1Tb4b4b4.Te6edf3nameTb4b4b4, Te6edf3rssi Tff7b72= Tff7b72-T79c0ff2T79c0ff0Tb4b4b4)
Tff7b72val Te6edf3afterRssiUpdate Tff7b72= Te6edf3awaitItemTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff3Tb4b4b4, Te6edf3afterRssiUpdateTb4b4b4.Te6edf3sizeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3device1Tb4b4b4.Te6edf3addressTb4b4b4, Tb4b4b4(Te6edf3afterRssiUpdateTff7b72[T79c0ff1Tff7b72] Tff7b72as Te6edf3DeviceListEntryTb4b4b4.Te6edf3BleTb4b4b4)Tb4b4b4.Te6edf3addressTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72-T79c0ff2T79c0ff0Tb4b4b4, Tb4b4b4(Te6edf3afterRssiUpdateTff7b72[T79c0ff1Tff7b72] Tff7b72as Te6edf3DeviceListEntryTb4b4b4.Te6edf3BleTb4b4b4)Tb4b4b4.Te6edf3deviceTb4b4b4.Te6edf3rssiTb4b4b4)

Te6edf3cancelAndIgnoreRemainingEventsTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`bleDevicesForUi keeps the selected device visible even when not seen via scan`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3bondedBle Tff7b72= Te6edf3FakeBleDeviceTb4b4b4(Te6edf3address Tff7b72= Ta5d6ff"Ta5d6ff0D:0E:0F:10:11:12Ta5d6ff"Tb4b4b4, Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffBonded CTa5d6ff"Tb4b4b4, Te6edf3rssi Tff7b72= Tff7b72nullTb4b4b4)
Tff7b72val Te6edf3bondedDevice Tff7b72= Te6edf3DeviceListEntryTb4b4b4.Te6edf3BleTb4b4b4(Te6edf3device Tff7b72= Te6edf3bondedBleTb4b4b4, Te6edf3bonded Tff7b72= Tff7b72trueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3bleDevicesForUiTb4b4b4.Te6edf3test Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4)

T8b949e// The device is bonded and selected (e.g. auto-reconnect on launch); while connected it stops
T8b949e// advertising, so a scan never sees it β€” but it must stay visible so the user can disconnect.
Te6edf3harnessTb4b4b4.Te6edf3currentDeviceAddressFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3bondedDeviceTb4b4b4.Te6edf3fullAddress
Te6edf3baseDevicesFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3DiscoveredDevicesTb4b4b4(Te6edf3bleDevices Tff7b72= Te6edf3listOfTb4b4b4(Te6edf3bondedDeviceTb4b4b4)Tb4b4b4)

Tff7b72val Te6edf3items Tff7b72= Te6edf3awaitItemTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3itemsTb4b4b4.Te6edf3sizeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3bondedDeviceTb4b4b4.Te6edf3fullAddressTb4b4b4, Te6edf3itemsTff7b72[T79c0ff0Tff7b72]Tb4b4b4.Te6edf3fullAddressTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3itemsTff7b72[T79c0ff0Tff7b72]Tb4b4b4.Te6edf3bondedTb4b4b4)

Te6edf3cancelAndIgnoreRemainingEventsTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`stopBleScan does not clear scanned devices`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3device Tff7b72= Te6edf3FakeBleDeviceTb4b4b4(Te6edf3address Tff7b72= Ta5d6ff"Ta5d6ff01:02:03:04:05:06Ta5d6ff"Tb4b4b4, Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffNodeTa5d6ff"Tb4b4b4, Te6edf3rssi Tff7b72= Tff7b72-T79c0ff5T79c0ff0Tb4b4b4)
Tff7b72val Te6edf3scanFlow Tff7b72= Te6edf3MutableStateFlowTff7b72<Te6edf3BleDevice?Tff7b72>Tb4b4b4(Tff7b72nullTb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3bleScannerTb4b4b4.Te6edf3scanTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns Te6edf3scanFlowTb4b4b4.Te6edf3filterNotNullTb4b4b4(Tb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3bleDevicesForUiTb4b4b4.Te6edf3test Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)
Te6edf3scanFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3device
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3awaitItemTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3sizeTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3stopBleScanTb4b4b4(Tb4b4b4)
T8b949e// Should not emit a new empty list
Te6edf3expectNoEventsTb4b4b4(Tb4b4b4)

Te6edf3cancelAndIgnoreRemainingEventsTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

T8b949e// ── Mutual exclusion: only one of BLE / Network scanning may be active at a time ──────────

Tf0883e@Test
Tff7b72fun Td2a8ff`startBleScan cancels active network scan`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3viewModelTb4b4b4.Te6edf3startNetworkScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`startNetworkScan cancels active ble scan`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3startNetworkScanTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`selecting BLE stops active network scan`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3viewModelTb4b4b4.Te6edf3startNetworkScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3selectTransportTb4b4b4(Te6edf3DeviceTypeTb4b4b4.Te6edf3BLETb4b4b4)

Te6edf3assertEqualsTb4b4b4(Te6edf3DeviceTypeTb4b4b4.Te6edf3BLETb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3activeTransportTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`selecting TCP stops active ble scan`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3selectTransportTb4b4b4(Te6edf3DeviceTypeTb4b4b4.Te6edf3TCPTb4b4b4)

Te6edf3assertEqualsTb4b4b4(Te6edf3DeviceTypeTb4b4b4.Te6edf3TCPTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3activeTransportTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`selecting USB stops all active scans`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3selectTransportTb4b4b4(Te6edf3DeviceTypeTb4b4b4.Te6edf3USBTb4b4b4)

Te6edf3assertEqualsTb4b4b4(Te6edf3DeviceTypeTb4b4b4.Te6edf3USBTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3activeTransportTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3startNetworkScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3selectTransportTb4b4b4(Te6edf3DeviceTypeTb4b4b4.Te6edf3USBTb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

T8b949e// Manual scan control.

Tf0883e@Test
Tff7b72fun Td2a8ff`startBleScan succeeds while Connected`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4)
Te6edf3testSchedulerTb4b4b4.Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`startNetworkScan succeeds while Connected`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4)
Te6edf3testSchedulerTb4b4b4.Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3startNetworkScanTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`startBleScan succeeds while Connecting`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectingTb4b4b4)
Te6edf3testSchedulerTb4b4b4.Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`startBleScan succeeds while DeviceSleep`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3DeviceSleepTb4b4b4)
Te6edf3testSchedulerTb4b4b4.Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`connectionState transition to Connected cancels active ble scan`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4)
Te6edf3testSchedulerTb4b4b4.Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`connectionState transition to Connected cancels active network scan`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3viewModelTb4b4b4.Te6edf3startNetworkScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4)
Te6edf3testSchedulerTb4b4b4.Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`startBleAutoScan skips when device already selected`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3harnessTb4b4b4.Te6edf3currentDeviceAddressFlowTb4b4b4.Te6edf3value Tff7b72= Ta5d6ff"Ta5d6ffx01:02:03:04:05:06Ta5d6ff"

Te6edf3viewModelTb4b4b4.Te6edf3startBleAutoScanTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`startBleAutoScan starts when no device selected`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3harnessTb4b4b4.Te6edf3currentDeviceAddressFlowTb4b4b4.Te6edf3value Tff7b72= Tff7b72null

Te6edf3viewModelTb4b4b4.Te6edf3startBleAutoScanTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`startBleAutoScan starts only when active transport is BLE`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3viewModelTb4b4b4.Te6edf3selectTransportTb4b4b4(Te6edf3DeviceTypeTb4b4b4.Te6edf3TCPTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3startBleAutoScanTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3selectTransportTb4b4b4(Te6edf3DeviceTypeTb4b4b4.Te6edf3BLETb4b4b4)
Te6edf3viewModelTb4b4b4.Te6edf3startBleAutoScanTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`startNetworkAutoScan starts only when active transport is TCP`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3viewModelTb4b4b4.Te6edf3startNetworkAutoScanTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3selectTransportTb4b4b4(Te6edf3DeviceTypeTb4b4b4.Te6edf3TCPTb4b4b4)
Te6edf3viewModelTb4b4b4.Te6edf3startNetworkAutoScanTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

T8b949e// ── Toggle persistence: enable clears the opposite persisted auto-scan pref ──────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`toggleBleScan enabling scan clears networkAutoScan`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3harnessTb4b4b4.Te6edf3uiPrefsTb4b4b4.Te6edf3setNetworkAutoScanTb4b4b4(Tff7b72trueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3networkAutoScanTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3toggleBleScanTb4b4b4(Tb4b4b4)

T8b949e// Successful enable persisted bleAutoScan=true AND cleared the opposite pref to mirror the runtime
T8b949e// mutual-exclusion invariant in persisted state.
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3bleAutoScanTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3networkAutoScanTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`toggleNetworkScan enabling scan clears bleAutoScan`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3harnessTb4b4b4.Te6edf3uiPrefsTb4b4b4.Te6edf3setBleAutoScanTb4b4b4(Tff7b72trueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3bleAutoScanTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3toggleNetworkScanTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3networkAutoScanTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3bleAutoScanTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

T8b949e// ── onSelected stops active scan before connection setup ────────────────────────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`onSelected bonded BLE stops active scan before changing device`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3entry Tff7b72=
Te6edf3DeviceListEntryTb4b4b4.Te6edf3BleTb4b4b4(Te6edf3device Tff7b72= Te6edf3FakeBleDeviceTb4b4b4(Te6edf3address Tff7b72= Ta5d6ff"Ta5d6ff01:02:03:04:05:06Ta5d6ff"Tb4b4b4, Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffNodeTa5d6ff"Tb4b4b4)Tb4b4b4, Te6edf3bonded Tff7b72= Tff7b72trueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3startBleScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3onSelectedTb4b4b4(Te6edf3entryTb4b4b4)
Te6edf3testSchedulerTb4b4b4.Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// stopAllScans() runs before changeDeviceAddress() in onSelected β€” scan flag flips to false and
T8b949e// the radio controller sees the new address.
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3entryTb4b4b4.Te6edf3fullAddressTb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`onSelected TCP stops active network scan before changing device`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3entry Tff7b72= Te6edf3DeviceListEntryTb4b4b4.Te6edf3TcpTb4b4b4(Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffTCP NodeTa5d6ff"Tb4b4b4, Te6edf3fullAddress Tff7b72= Ta5d6ff"Ta5d6fft192.168.1.50Ta5d6ff"Tb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3startNetworkScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3onSelectedTb4b4b4(Te6edf3entryTb4b4b4)
Te6edf3testSchedulerTb4b4b4.Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3entryTb4b4b4.Te6edf3fullAddressTb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`onSelected records active transport for BLE TCP and USB entries`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3bleEntry Tff7b72=
Te6edf3DeviceListEntryTb4b4b4.Te6edf3BleTb4b4b4(Te6edf3device Tff7b72= Te6edf3FakeBleDeviceTb4b4b4(Te6edf3address Tff7b72= Ta5d6ff"Ta5d6ff01:02:03:04:05:06Ta5d6ff"Tb4b4b4, Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffBLE NodeTa5d6ff"Tb4b4b4)Tb4b4b4, Te6edf3bonded Tff7b72= Tff7b72trueTb4b4b4)
Tff7b72val Te6edf3tcpEntry Tff7b72= Te6edf3DeviceListEntryTb4b4b4.Te6edf3TcpTb4b4b4(Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffTCP NodeTa5d6ff"Tb4b4b4, Te6edf3fullAddress Tff7b72= Ta5d6ff"Ta5d6fft192.168.1.50Ta5d6ff"Tb4b4b4)
Tff7b72val Te6edf3usbEntry Tff7b72=
Te6edf3DeviceListEntryTb4b4b4.Te6edf3UsbTb4b4b4(
Te6edf3usbData Tff7b72= Tff7b72object Tb4b4b4: T56d364orgTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3featureTb4b4b4.Te6edf3connectionsTb4b4b4.Te6edf3modelTb4b4b4.Te6edf3UsbDeviceData Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffUSB NodeTa5d6ff"Tb4b4b4,
Te6edf3fullAddress Tff7b72= Ta5d6ff"Ta5d6ffs/dev/bus/usb/001/002Ta5d6ff"Tb4b4b4,
Te6edf3bonded Tff7b72= Tff7b72trueTb4b4b4,
Tb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3onSelectedTb4b4b4(Te6edf3bleEntryTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DeviceTypeTb4b4b4.Te6edf3BLETb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3activeTransportTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3onSelectedTb4b4b4(Te6edf3tcpEntryTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DeviceTypeTb4b4b4.Te6edf3TCPTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3activeTransportTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3onSelectedTb4b4b4(Te6edf3usbEntryTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DeviceTypeTb4b4b4.Te6edf3USBTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3activeTransportTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

T8b949e// ── persistNetworkAutoScanIntent invariant ───────────────────────────────────────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`persistNetworkAutoScanIntent true clears bleAutoScan`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3harnessTb4b4b4.Te6edf3uiPrefsTb4b4b4.Te6edf3setBleAutoScanTb4b4b4(Tff7b72trueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3bleAutoScanTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3persistNetworkAutoScanIntentTb4b4b4(Tff7b72trueTb4b4b4)

T8b949e// Persisting a network-auto-scan intent must clear the opposite BLE pref so persisted state
T8b949e// mirrors the runtime mutual-exclusion invariant β€” at most one of the two may be true.
Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3bleAutoScanTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3networkAutoScanTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

T8b949e// ── connectToManualAddress stops scans before changing device ────────────────────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`connectToManualAddress stops active network scan and changes device address`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3viewModelTb4b4b4.Te6edf3startNetworkScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3connectToManualAddressTb4b4b4(Ta5d6ff"Ta5d6fft192.168.1.99Ta5d6ff"Tb4b4b4)
Te6edf3testSchedulerTb4b4b4.Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Tff7b72falseTb4b4b4, Te6edf3viewModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6fft192.168.1.99Ta5d6ff"Tb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8fffailingScanFlowTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3flowTff7b72<Te6edf3BleDeviceTff7b72> Tb4b4b4{
Tff7b72throw Te6edf3BleScanStartExceptionTb4b4b4(
Te6edf3reason Tff7b72= Te6edf3BleScanStartFailureReasonTb4b4b4.Te6edf3ApplicationRegistrationFailedTb4b4b4,
Te6edf3cause Tff7b72= Te6edf3IllegalStateExceptionTb4b4b4(Ta5d6ff"Ta5d6ffFailed to start scan as app cannot be registeredTa5d6ff"Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

T8b949e/**
* Loads the scan-failure strings into the compose-resources cache up front.
*
* The first read of a resource completes on an internal `Dispatchers.Default` scope, so an un-warmed lookup lands after
* the caller has moved on. Warming keeps the error message observable synchronously, which these tests need because
* they also assert on exact virtual-time retry cooldowns and so cannot wait in real time.
*
* Best-effort via [safeCatchingAll], mirroring the ViewModel: the androidHostTest stubs leave `Resources.getSystem()`
* unmocked and skiko's initializer can raise an `Error` here, so resources never resolve and the untranslated fallback
* β€” identical text, produced synchronously β€” is what the assertions match. Cancellation still propagates.
*/
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffwarmScanFailureStringsTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3safeCatchingAll Tb4b4b4{
Te6edf3getStringSuspendTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3bluetooth_scan_start_failedTb4b4b4)
Te6edf3getStringSuspendTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3bluetooth_scan_missing_permissionTb4b4b4)
Te6edf3getStringSuspendTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3bluetooth_disabledTb4b4b4)
Te6edf3getStringSuspendTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3bluetooth_scan_location_services_disabledTb4b4b4)
Te6edf3getPluralStringSuspendTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3pluralsTb4b4b4.Te6edf3bluetooth_scan_too_frequentTb4b4b4, T79c0ff1Tb4b4b4, T79c0ff1LTb4b4b4)
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.2s